Skip to content

Error Boundaries

Video Summary

In this example, we looked at a financial news website with a stock ticker:

Screenshot of the default output

We have a Ticker component that fetches fake data from our test API, using the SWR library:

import React from 'react';
import useSWR from 'swr';
const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-stock-quotes';
async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
return json.data;
}
function Ticker() {
const { data, isLoading } = useSWR(
ENDPOINT,
fetcher
);
if (isLoading) {
return null;
}
return (
<section className="ticker">
<dl>
{data.map(
({ id, title, value, change }) => (
<React.Fragment key={id}>
{/* ✂️ UI stuff */}
</React.Fragment>
)
)}
</dl>
</section>
);
}

This code works fine in the “happy path”, but what if things go wrong? What if the server can't provide the data we're requesting?

We can test this with the simulatedError query param:

const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-stock-quotes?simulatedError=true';

When we do this, we get an error message:

TypeError: Cannot read properties of undefined (reading 'map')

The problem is that we don't have any data, because the server returned an error. Our component hasn't implemented any error-handling.

We can and should add error-handling, but honestly, it's hard to anticipate all possible errors. Almost by definition, errors are rare events, edge cases that are hard to predict.

This component, the <Ticker> that shows the stock prices, is a relatively unimportant part of this page. Most people who visit this page are probably here for the top stories, or to cancel a subscription, or any number of other things.

And so it seems like a shame that an issue in an unimportant component is breaking the entire experience. What if we could "quarantine" the issues here, so that this error only affects the <Ticker> component?

This is what Error Boundaries allow us to do.

I created a custom ErrorBoundary component. We'll dig into it shortly, but first, here's how we implement it:

import React from 'react';
import Header from './Header';
import Ticker from './Ticker';
import Stories from './Stories';
import ErrorBoundary from './ErrorBoundary';
function App() {
return (
<>
<Header />
<ErrorBoundary
fallback={
<div className="error">
Stock ticker couldn't load
</div>
}
>
<Ticker />
</ErrorBoundary>
<main>
<h1>Top Stories</h1>
<Stories />
</main>
</>
);
}

We're wrapping the <Ticker> element inside our ErrorBoundary. As a result, we'll show a custom fallback message if the Ticker component explodes:

Screenshot of the default output

Unfortunately, error boundaries are one of the few features that has not been migrated to hooks. We still need to use class components for our error boundaries.

The good news is that I generally only create a single ErrorBoundary component per codebase, and reuse this component whenever I need a new error boundary. And so we won't need to actually work with class components at all, once we've set this up.

Here's the code:

import React from 'react';
class ErrorBoundary extends React.Component {
// Equivalent to:
// const [hasError, setHasError] = useState(false);
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error);
console.info({ errorInfo });
}
render() {
if (this.state.hasError) {
return this.props.fallback || null;
}
return this.props.children;
}
}
export default ErrorBoundary;

We won't dig into this too much, since it isn't relevant to understand that much about class components. But here's the quick summary:

  • constructor is used to initialize state variables.
  • render is where all of the render stuff goes. This method is equivalent to everything inside our functional component except the hooks.
  • componentDidCatch allows us to log when errors happen
  • getDerivedStateFromError implements the error boundary.

When an error happens within any descendant component, the getDerivedStateFromError method intercepts it. Instead of blowing up the whole page, it flips our state variable, hasError, from false to true.

In the render method, we render the children prop by default (the <Ticker> element), but if hasError is true, we render the fallback content instead, if provided.

This handy component can be used to "wall off" different sections of our application, guaranteeing that problems in one section don't affect the other. As a result, small problems in our code don't cause big problems for our users!

Here's the sandbox from the video:

Code Playground

import React from 'react';

import Header from './Header';
import Ticker from './Ticker';
import Stories from './Stories';
import ErrorBoundary from './ErrorBoundary';

function App() {
return (
<>
<Header />
<ErrorBoundary>
<Ticker />
</ErrorBoundary>
<main>
<h1>Top Stories</h1>
<Stories />
</main>
</>
);
}

export default App;

Something went wrong

/Ticker.js: Cannot read properties of undefined (reading 'map') (28:12) 25 | return ( 26 | <section className="ticker"> 27 | <dl> > 28 | {data.map(({ id, title, value, change }) => ( ^ 29 | <React.Fragment key={id}> 30 | <dt>{title}</dt> 31 | <dd>

  1. Cannot read properties of undefined (reading 'map')
  2. The above error occurred in the <Ticker> component: at Ticker (https://sandpack-bundler.vercel.app/Ticker.js:35:36) at ErrorBoundary (https://sandpack-bundler.vercel.app/ErrorBoundary.js:16:5) at App React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
  3. TypeError: Cannot read properties of undefined (reading 'map') at Ticker (https://sandpack-bundler.vercel.app/Ticker.js:46:22) at renderWithHooks (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:16305:18) at updateFunctionComponent (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:19588:20) at beginWork (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:21601:16) at beginWork$1 (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:27426:14) at performUnitOfWork (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:26557:12) at workLoopSync (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:26466:5) at renderRootSync (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:26434:7) at recoverFromConcurrentError (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:25850:20) at performSyncWorkOnRoot (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:26096:20) at flushSyncCallbacks (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:12042:22) at eval (https://sandpack-bundler.vercel.app/node_modules/react-dom/cjs/react-dom.development.js:25651:13)
  4. {
    "errorInfo": {
    "componentStack": "\n at Ticker (https://sandpack-bundler.vercel.app/Ticker.js:35:36)\n at ErrorBoundary (https://sandpack-bundler.vercel.app/ErrorBoundary.js:16:5)\n at App"
    }
    }
  5. Could not consume error:
    [[Error]]

Error tracking

Error boundaries allow us to soften the blow of an unexpected code failure, but what if we wanted to be notified about them, so that we can fix the problem altogether for future users?

Error trackers like Sentry and LogRocket can help us catalog these unexpected issues. They'll let us see how often the issues are occurring, and provide a bunch of metadata to help us understand and fix the problem.

The Sentry SDK even has an ErrorBoundary component. It's quite a bit like the one we saw in this lesson, but it also collects data about the failure for us.

Error tracking is beyond the scope of this course, but feel free to dig into this if you're interested in this topic!

Drawing logical boundaries

One of the cool things about error boundaries is that they wrap around the entire slice of the React tree. They'll catch errors that are thrown in children, grandchildren, great-grandchildren… All the way down.

For example, suppose that we refactor our news-reader app, and now there are several components involved in showing the real-time market prices:

function App() {
return (
<>
<Header />
<ErrorBoundary>
<RealTimeInfo />
</ErrorBoundary>
<Stories />
</>
);
}
function RealTimeInfo() {
return (
<Ticker />
);
}
function Ticker() {
const { data, isLoading } = useSWR(ENDPOINT, fetcher);
return data.map(item => (
<Price key={item.id} item={item} />
));
}
function Price({ item }) {
// ✂️ Display the item info
}

RealTimeInfo is the component being rendered within the <ErrorBoundary>, but the boundary also wraps around Ticker and Price. If an error is thrown in any of these components, it'll be caught by the error boundary.

If we draw out our React tree, the boundary wraps around this entire slice of the app:

Diagram showing an error boundary drawn around part of an element tree

I like to think about it like a force-field that protects the rest of the app from any explosions that happen inside this slice of the tree. 💥

We're even allowed to nest error boundaries. When an error is thrown, it bubbles up through the tree until it hits the nearest boundary:

Diagram showing nested error boundaries

Suppose an error is thrown inside <Comment />. It bubbles up to the nearest error boundary, which wraps around <Discussion />. We'll render some sort of fallback UI in lieu of the comment section, but the article itself will still be accessible.

This is a good example of how we can use error boundaries strategically. Our goal is to minimize the damage when things go wrong.

Brandon Dail has a wonderful blog post: “The Fault in Our Tolerance: Accounting for Failures in React” . This blog post digs deep into this exact question, looking at where to apply error boundaries in several real-world scenarios. I highly recommend checking it out, right now.

To provide a real-world example of my own, here are some of ways I use error boundaries in this course platform:

  • Each playground is wrapped in an error boundary, so that explosions in one playground don't break the entire lesson.
  • The “Create Note” drawer is wrapped in an error boundary, in case there's some unexpected issue with the notes feature.
  • Some lessons have multiple "pages". Each page is wrapped in an error boundary, so that a broken lesson segment won't torch the entire lesson.

For more information on error boundaries, check out the official docs.